package com.listeningframework.boot.autoconfigure.orm.jpa.search;

import com.google.common.collect.Lists;
import com.listeningframework.boot.autoconfigure.orm.jpa.search.filters.*;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.Assert;

import javax.persistence.criteria.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class Specifications<T> implements Specification<T> {
    /**
     * Common Operations
     */
    private final List<String> COMMON_OPERATIONS = getCommonOperations();
    /**
     * Collection<SearchFilter<?>>
     */
    private Collection<SearchFilter<?>> filters = Lists.newArrayList();

    /**
     * Common Operations
     */
    private List<String> getCommonOperations() {
        List<String> operationsList = Lists.newArrayList();
        operationsList.add(Operations.EQ.name());
        operationsList.add(Operations.NE.name());
        operationsList.add(Operations.IS_NULL.name());
        operationsList.add(Operations.NOT_NULL.name());
        operationsList.add(Operations.MUILTI_OR.name());
        operationsList.add(Operations.NOT_IN.name());
        operationsList.add(Operations.IN.name());
        return operationsList;
    }

    public Specifications(final Collection<SearchFilter<?>> filters, Class<T> clz) {
        Assert.notNull(clz, "param clz required");
        this.filters.clear();
        this.filters.addAll(filters);
    }

    /**
     * handle Common Search operations
     */
    private void handleCommonSearch(Root<T> root, List<Predicate> predicates, CriteriaBuilder builder,
                                    SearchFilter<?> filter) {
        String[] names = StringUtils.split(filter.getName(), ".");
        Path<String> expression = root.get(names[0]);
        for (int i = 1; i < names.length; i++) {
            expression = expression.get(names[i]);
        }
        List<?> valueList = filter.getValue();
        Object filterValue = valueList.get(0);
        // logic operator
        switch (filter.getOperations()) {
            case EQ:
                predicates.add(builder.equal(expression, filterValue));
                break;
            case NE:
                predicates.add(builder.notEqual(expression, filterValue));
                break;
            case IS_NULL:
                predicates.add(builder.isNull(expression));
                break;
            case NOT_NULL:
                predicates.add(builder.isNotNull(expression));
                break;
            case OR:
                predicates.add(builder.or(builder.equal(expression, filterValue)));
                break;
            case MUILTI_OR:
                addOR(predicates, builder, expression, filterValue);
                break;
            case IN:
                predicates.add(expression.in(filterValue));
                break;
            case NOT_IN:
                predicates.add(builder.not(expression.in(filterValue)));
                break;
            default:
                break;
        }
    }

    /**
     * Handle String Search
     */
    private void handleStringSearch(Root<T> root, List<Predicate> predicates, CriteriaBuilder builder,
                                    StringSearchFilter filter) {
        String[] names = StringUtils.split(filter.getName(), ".");
        Path<String> expression = root.get(names[0]);
        for (int i = 1; i < names.length; i++) {
            expression = expression.get(names[i]);
        }
        List<String> valueList = filter.getValue();
        String filterValue = valueList.get(0);
        // logic operator
        switch (filter.getOperations()) {
            case LIKE:
                predicates.add(builder.like(expression, "%" + filterValue + "%"));
                break;
            default:
                break;
        }
    }

    /**
     * Handle BigDecimal Search
     */
    private void handleBigDecimalSearch(Root<T> root, List<Predicate> predicates, CriteriaBuilder builder,
                                        BigDecimalSearchFilter filter) {
        String[] names = StringUtils.split(filter.getName(), ".");
        Path<BigDecimal> expression = root.get(names[0]);
        for (int i = 1; i < names.length; i++) {
            expression = expression.get(names[i]);
        }
        List<BigDecimal> valueList = filter.getValue();
        BigDecimal filterValue = valueList.get(0);
        handleNumSearch(expression, predicates, builder, filterValue, filter);
    }

    /**
     * Handle Long Search
     */
    private void handleLongSearch(Root<T> root, List<Predicate> predicates, CriteriaBuilder builder,
                                  LongSearchFilter filter) {
        String[] names = StringUtils.split(filter.getName(), ".");
        Path<Long> expression = root.get(names[0]);
        for (int i = 1; i < names.length; i++) {
            expression = expression.get(names[i]);
        }
        List<Long> valueList = filter.getValue();
        Long filterValue = valueList.get(0);
        handleNumSearch(expression, predicates, builder, filterValue, filter);
    }

    /**
     * Handle Integer Search
     */
    private void handleIntegerSearch(Root<T> root, List<Predicate> predicates, CriteriaBuilder builder,
                                     IntegerSearchFilter filter) {
        String[] names = StringUtils.split(filter.getName(), ".");
        Path<Integer> expression = root.get(names[0]);
        for (int i = 1; i < names.length; i++) {
            expression = expression.get(names[i]);
        }
        List<Integer> valueList = filter.getValue();
        Integer filterValue = valueList.get(0);
        handleNumSearch(expression, predicates, builder, filterValue, filter);
    }

    /**
     * Handle Number Search
     */
    private <N extends Comparable<N>> void handleNumSearch(Path<N> expression, List<Predicate> predicates,
                                                           CriteriaBuilder builder, N filterValue,
                                                           SearchFilter<?> filter) {
        switch (filter.getOperations()) {
            case GT:
                predicates.add(builder.greaterThan(expression, filterValue));
                break;
            case LT:
                predicates.add(builder.lessThan(expression, filterValue));
                break;
            case GTE:
                predicates.add(builder.greaterThanOrEqualTo(expression, filterValue));
                break;
            case LTE:
                predicates.add(builder.lessThanOrEqualTo(expression, filterValue));
                break;
            default:
                break;
        }
    }

    /**
     * OR 参数拼接
     *
     * @param predicates Predicate列表
     * @param builder    CriteriaBuilder
     * @param expression Path
     * @param params     参数
     */
    private void addOR(List<Predicate> predicates, CriteriaBuilder builder, Path<?> expression, Object... params) {
        List<Predicate> tempOR = new ArrayList<>();
        if (params == null || params.length <= 0) {
            return;
        }
        for (Object val : params) {
            tempOR.add(builder.equal(expression, val));
        }
        predicates.add(builder.or(tempOR.toArray(new Predicate[tempOR.size()])));
    }

    @Override
    public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) {
        if (CollectionUtils.isEmpty(filters)) {
            return builder.conjunction();
        }

        List<Predicate> predicates = Lists.newArrayList();
        for (SearchFilter<?> filter : filters) {
            if (COMMON_OPERATIONS.contains(filter.getOperations().name())) {
                handleCommonSearch(root, predicates, builder, filter);
            } else if (filter.isClass(String.class)) {
                StringSearchFilter search = (StringSearchFilter) filter;
                handleStringSearch(root, predicates, builder, search);
            } else if (filter.isClass(BigDecimal.class)) {
                BigDecimalSearchFilter search = (BigDecimalSearchFilter) filter;
                handleBigDecimalSearch(root, predicates, builder, search);
            } else if (filter.isClass(Long.class)) {
                LongSearchFilter search = (LongSearchFilter) filter;
                handleLongSearch(root, predicates, builder, search);
            } else if (filter.isClass(Integer.class)) {
                IntegerSearchFilter search = (IntegerSearchFilter) filter;
                handleIntegerSearch(root, predicates, builder, search);
            }
        }

        // 将所有条件用 and 联合起来
        if (!predicates.isEmpty()) {
            return builder.and(predicates.toArray(new Predicate[predicates.size()]));
        }
        return builder.conjunction();
    }
}
